package paim.wingchun.model.modelos;


import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;

import org.hibernate.HibernateException;
import org.hibernate.Session;

import com.google.common.collect.Collections2;

import paim.wingchun.app.Reflections;
import paim.wingchun.app.WC_Log;
import paim.wingchun.model.DeleteException;
import paim.wingchun.model.Funcoes;
import paim.wingchun.model.Recognizable;
import paim.wingchun.model.DAO.DAO;
import paim.wingchun.model.DAO.DAOException;
import paim.wingchun.model.modelos.ErroModel.Tipo;
import paim.wingchun.model.pojos.Erro;
import paim.wingchun.model.pojos.Pojo;
import paim.wingchun.model.validadores.Validador;
import paim.wingchun.view.Dialogs;

import paim.gh.app.GHApp;

public class Model<P extends Pojo> implements Recognizable<P> {

    public static final boolean OTIMIZADO = false;

    /** Instancia do modelo */
    /** Singleton Lazy initialization http://howtodoinjava.com/2012/10/22/singleton-design-pattern-in-java/ */
    private static volatile Model<? extends Pojo> instance;

    protected Class<P> classeP;

    private DAO<P> dao;

    protected Model() {}

    /** Retorna instancia de Modelo (padrao Singleton) */
    public static/* synchronized */Model<? extends Pojo> getInstance() {
        if ( instance == null )
            synchronized ( Model.class ) {
                /* Double check */
                if ( instance == null )
                    instance = new Model<>();
            }

        return instance;
    }

    @Override
    public final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

    /** retorna classe do pojo correspondente ao modelo */
    @Override
    public Class<P> getpClass() {
        if ( classeP != null )
            return classeP;

        classeP = Reflections.getClassParameter(getClass(), Pojo.class);
        return classeP;
    }

    @SuppressWarnings("unchecked")
    protected final <D extends DAO<P>> D getDAO() throws DAOException {
        if ( dao != null )
            return (D) dao;

        try {
            dao = Reflections.getDAO(getClass());
        }
        catch ( Exception e ) {
            throw new DAOException(e);
        }
        return (D) dao;
    }

    @SuppressWarnings("unchecked")
    public final P get(Session session, long id) throws Exception {
        id = before_get(session, id);
        P pojo = (P) getDAO().get(session, getpClass(), id);
        pojo = after_get(session, pojo);

        return pojo;
    }

    protected long before_get(Session session, long id) {
        return id;
    }

    protected P after_get(Session session, P pojo) {
        return pojo;
    }

    /** Recupera lista de todos pojos referente a este modelo. Acessa o respectivo DAO procurando todos os pojos
     * persistidos no Banco de Dados. */
    public final List<Pojo> list(Session session) throws Exception {
        before_list(session);
        List<Pojo> pojos = getDAO().list(session, getpClass());
        pojos = after_list(session, pojos);

        return pojos;
    }

    protected void before_list(Session session) {}

    protected List<Pojo> after_list(Session session, List<Pojo> pojos) {
        return pojos;
    }

    /** Persiste um Pojo ou uma Lista de Pojos */
    @SuppressWarnings("unchecked")
    public final List<Erro> saveOrUpdate(Session session, Object objeto) throws Exception {
        objeto = before_saveOrUpdate(session, objeto);

        try {
            /** se for um pojo */
            if ( Pojo.class.isAssignableFrom(objeto.getClass()) )
                getDAO().saveOrUpdate(session, (Pojo) objeto);

            /** se for uma lista */
            else if ( List.class.isAssignableFrom(objeto.getClass()) )
                getDAO().saveOrUpdate(session, (List<Pojo>) objeto);

            else
                throw new ClassCastException();

        }
        catch ( Exception e ) {
            throw new DeleteException(e);
        }

        return after_saveOrUpdate(session, objeto, new ArrayList<Erro>());
    }

    protected Object before_saveOrUpdate(Session session, Object objeto) {
        return objeto;
    }

    protected List<Erro> after_saveOrUpdate(Session sessao, Object objeto, List<Erro> erros) {
        return erros;
    }

    /** Valida o pojo e retorna lista de erros gerados pela validacao. */
    @SuppressWarnings("unused")
    public final List<Erro> validar(Session sessao, Pojo pojo) {
        /* nao valida se nao houver pendencias */
        if ( Model.OTIMIZADO && pojo.getPendencia() == 0 )
            return new ArrayList<Erro>();

        /* limpas os erros do Pojo pois sera validado */
        pojo.getErros().clear();

        pojo = antesValidar(sessao, pojo);

        List<Erro> erros = new ArrayList<Erro>();
        try {
            erros.addAll(Validador.naoNulo(pojo));
            erros.addAll(Validador.naoNegativo(pojo));
            erros.addAll(Validador.validaData(pojo));
            erros.addAll(Validador.validaCampo(pojo));

            erros = aposValidar(sessao, pojo, erros);
        }
        catch ( Exception e ) {

            WC_Log.getInstancia().getlogger().log(Level.SEVERE, e.getMessage(), e);
            Dialogs.erro("Sistema não conseguiu validar o pojo, contate criador", "Validação");
            Erro erro = new Erro(0L, ErroModel.Level.ERRO, Tipo.SISTEMA);
            erro.setNome("Validação imcompleta");
            erro.setMensagem("Sistema não conseguiu validar o pojo, contate criador, \n objeto pendente de validação");
            erros.add(erro);
        }
        /* se nao houverem erros zera as pendencias do pojo */
        if ( erros.isEmpty() )
            pojo.setPendencia(0);
        else {
            for ( Erro e : erros ) {
                /* se achar um erro mais alto, entao seta no pojo como pendencia */
                if ( pojo.getPendencia() < e.getLevel().getValor() ) {
                    pojo.setPendencia(e.getLevel().getValor());
                    /* se for o erro mais alto, entao nao precisa mais olhar os outros da lista */
                    if ( e.getLevel() == ErroModel.Level.ERRO$PRIORITARIO )
                        break;
                }
            }
        }
        return erros;
    }

    protected Pojo antesValidar(Session session, Pojo pojo) {
        return pojo;
    }

    protected List<Erro> aposValidar(Session session, Pojo pojo, List<Erro> erros) throws Exception {
        return erros;
    }

    /** serao executados se
     *
     * @author paim 28/04/2011
     * @param sessao
     * @param objeto
     * @param erros
     * @throws DeleteException
     *             void */
    @SuppressWarnings("unchecked")
    public final void delete(Session sessao, Object objeto, List<Erro> erros) throws DeleteException {

        if ( !before_delete(sessao, objeto, erros) )
            return;

        /* se tem impeditivos nao remove */
        if ( !Collections2.filter(erros, Funcoes.ERRO.impeditivos()).isEmpty() )
            throw new DeleteException("Existem erros impeditivos");

        try {
            /** se for um pojo */
            if ( Pojo.class.isAssignableFrom(objeto.getClass()) )
                getDAO().delete(sessao, (Pojo) objeto);

            /** se for uma lista */
            else if ( List.class.isAssignableFrom(objeto.getClass()) )
                getDAO().delete(sessao, (List<Pojo>) objeto);

            else
                throw new ClassCastException();

            after_delete();
        }
        catch ( Exception e ) {
            throw new DeleteException(e);
        }
    }

    /** se true metodos <BR>
     * {@link Model#delete(Session, Pojo, List)} e <BR>
     * {@link Model#after_delete(Session, boolean)} <BR>
     * serao executados se
     *
     * @author paim 28/04/2011
     * @param sessao
     * @param pojo
     * @param errosExclusao
     * @return boolean */
    protected boolean before_delete(Session sessao, Object pojo, List<Erro> errosExclusao) {
        return true;
    }

    protected void after_delete() {}

    public Object getValorAtributo(Session sessao, long id, String atributo) throws Exception {
        return getDAO().getValorAtributo(sessao, getpClass(), id, atributo);
    }

    public List<Long> ids(Session sessao) throws HibernateException, Exception {
        return getDAO().ids(sessao, getpClass());
    }

    @SuppressWarnings("unchecked")
    public <T> T reLoad(Session session, T objeto) throws HibernateException, Exception {
        if ( Pojo.class.isAssignableFrom(objeto.getClass()) ) {
            Pojo pojo = (Pojo) objeto;
            pojo = getDAO().reLoad(session, pojo);
            return (T) pojo;
        }
        else if ( List.class.isAssignableFrom(objeto.getClass()) ) {
            List<Pojo> pojos = (List<Pojo>) objeto;
            List<Pojo> recarregados = new ArrayList<Pojo>(pojos.size());

            for ( Pojo pojo : pojos ) {
                pojo = reLoad(session, pojo);
                recarregados.add(pojo);
            }
            return (T) recarregados;
        }
        else
            throw new ClassCastException();
    }

    public List<Pojo> complementary(Session session, List<Long> ids) throws Exception {
        List<Pojo> pojos = getDAO().complementary(session, getpClass(), ids);
        return pojos;
    }

    /** desengata um objeto para exclusao, baseado nos engates fornecidos
     *
     * @author paim 08/12/2011
     * @param session
     * @param pojo
     * @return boolean , se true desengate OK */
    public boolean disengage(Session session, Pojo pojo, List<Erro> engates) throws Exception {
        return true;
    }

    public boolean disengage(Session session, Pojo pojo) throws Exception {
        List<Erro> engates = couplings(session, pojo);
        return disengage(session, pojo, engates);
    }

    /** find engage
     *
     * @author paim 09/12/2011
     * @param session
     * @param pojo
     *            , objeto a ser analizado
     * @return List<Erro> , os engates- uma lista contendo os objetos que nao podem ser excluidos devido a engates
     * @throws Exception */
    public List<Erro> couplings(Session session, Pojo pojo) throws Exception {
        return Collections.emptyList();
    }

    /** remove um pojo do banco, tratando de eliminar os engates possiveis
     *
     * @author paim 09/12/2011 // * @param balde
     * @param pojo
     * @return boolean
     * @throws Exception */
    public boolean disengageAndRemove(/* Balde balde, */Session sessao, Pojo pojo) throws Exception {
        List<Erro> errosExclusao = new ArrayList<>();
        Model<?> modelo = Reflections.getModel(pojo.getClass());
        List<Erro> engates = modelo.couplings(sessao, pojo);

        if ( !engates.isEmpty() ) {
            if ( !Dialogs.perguntaMessageErros(GHApp.getInstancia().getVisaoApp(), engates) )
                return false;
            /* antes de remover tem que ver se pode fazer isso */
            if ( !modelo.disengage(sessao, pojo, engates) )
                return false;
        }

        delete(sessao, pojo, errosExclusao);

        return true;
    }

    /** busca os objetos desse modelo que estao ligados a esse pojo
     *
     * @author paim 09/12/2011
     * @param session
     * @param pojo
     * @return List
     *         <P> */
    public List<P> getObjetos(Session session, Pojo pojo) throws Exception {
        return getDAO().getObjetos(session, pojo);
    }

    @Deprecated
    public final List<Erro> mGetPendencias(Session sessao, int severidade) throws Exception {
        severidade = antesGetPendencias(sessao, severidade);
        List<Erro> erros = getPendencias(sessao, severidade);
        erros = aposGetPendencias(sessao, erros);

        return erros;
    }

    @Deprecated
    protected int antesGetPendencias(Session sessao, int severidade) {
        return severidade;
    }

    @Deprecated
    private List<Erro> getPendencias(Session sessao, int severidade) throws Exception {
        DAO<P> dao = getDAO();

        /* Busca POJOs com erros */
        List<Pojo> pojos = dao.list(sessao, this.getpClass(), severidade);

        List<Erro> erros = new ArrayList<Erro>();

        List<Pojo> limpos = new ArrayList<Pojo>();
        /* Revalida todos objetos pendentes */
        for ( Pojo pojo : pojos ) {
            List<Erro> errosPojo = new ArrayList<Erro>();
            for ( Erro e : this.validar(sessao, pojo) )
                errosPojo.add(e);

            /* Verifica se objeto continua com pendencias. Se continua, adiciona erros no total de erros retornados.
             * Senï¿½o, seta indicador de pendencia para zero e persiste objeto atualizado */
            if ( errosPojo.size() > 0 ) {
                erros.addAll(errosPojo);
                pojo.setPendencia(errosPojo.size());
            }
            else {
                pojo.setPendencia(0);
                limpos.add(pojo);
            }
        }
        dao.saveOrUpdate(sessao, limpos);
        /* Retorna total de erros encontrados */
        return erros;
    }

    @Deprecated
    protected List<Erro> aposGetPendencias(Session sessao, List<Erro> erros) {
        return erros;
    }

}
